Basic types
Field numbers
Are used to identify position inside the string byte. int32 id = 1; is not an asignment
Scalar data types
| Protobuf | C# | Notes |
|---|---|---|
| double | double | |
| float | float | |
| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | |
| int64 | long | |
| int64 | ||
| uint32 | ||
| uint64 | ||
| sint32 | ||
| sint64 | ||
| fixed32 | ||
| fixed64 | ||
| sfixed32 | int | |
| sfixed64 | int | |
| bool | bool | |
| string | string | |
| bytes | ByteString |
- The standard encoding for int32 and int64 is inefficient when you’re working with signed values. If your field is likely to contain negative numbers, use sint32 or sint64 instead. These types map to the C# int and long types, respectively.
- The fixed fields always use the same number of bytes no matter what the value is. This behavior makes serialization and deserialization faster for larger values.
- Protobuf strings are UTF-8 (or 7-bit ASCII) encoded. The encoded length can’t be greater than 232.
- The Protobuf runtime provides a ByteString type that maps easily to and from C# byte[] arrays
Enums
An enum must contain a constant that maps to zero as its first element. This is because:
- There must be a zero value, so that we can use 0 as a numeric default value.
- The zero value needs to be the first element, for compatibility with the proto2 semantics where the first enum value is always the default.
Enum values must be in the 32 bit integer range. If you need negative values, better use messages.
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
When using it inside the definition:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
Corpus corpus = 4;
}
Note that the .proto language allows multiple enum symbols to have the same numeric value. Symbols with the same numeric value are synonyms. These are represented in C# in exactly the same way, with multiple names corresponding to the same numeric value.
Other .NET primitives
Some of the C# types are not supported by default, but some of them are covered by the Google’s Well Known Types.
Well Known types - are types in the google.protobuf package API, provide code generation and runtime support for complex field types across the supported platforms. In other wordes, is a colection of reusable message types with implementations in all suported languages
Decimals
Protobuf doesn't natively support the .NET decimal type, just double and float. There's an ongoing discussion in the Protobuf project about the possibility of adding a standard Decimal type to the well-known types, with platform support for languages and frameworks that support it. Nothing has been implemented yet.
It's possible to create a message definition to represent the decimal type that would work for safe serialization between .NET clients and servers. But developers on other platforms would have to understand the format being used and implement their own handling for it.
Creating a custom decimal type for Protobuf
A simple implementation might be similar to the nonstandard Money type that some Google APIs use, without the currency field.
package CustomTypes;
// Example: 12345.6789 -> { units = 12345, nanos = 678900000 }
message DecimalValue {
// Whole units part of the amount
int64 units = 1;
// Nano units of the amount (10^-9)
// Must be same sign as units
sfixed32 nanos = 2;
}
The nanos field represents values from 0.999_999_999 to -0.999_999_999. For example, the decimal value 1.5m would be represented as { units = 1, nanos = 500_000_000 }. This is why the nanos field in this example uses the sfixed32 type, which encodes more efficiently than int32 for larger values. If the units field is negative, the nanos field should also be negative.
[!NOTE] There are multiple other algorithms for encoding
decimalvalues as byte strings, but this message is easier to understand than any of them. The values are not affected by endianness on different platforms.
Conversion between this type and the BCL decimal type might be implemented in C# like this:
namespace CustomTypes;
public partial class DecimalValue
{
private const decimal NanoFactor = 1_000_000_000;
public DecimalValue(long units, int nanos)
{
Units = units;
Nanos = nanos;
}
public static implicit operator decimal(CustomTypes.DecimalValue grpcDecimal)
{
return grpcDecimal.Units + grpcDecimal.Nanos / NanoFactor;
}
public static implicit operator CustomTypes.DecimalValue(decimal value)
{
var units = decimal.ToInt64(value);
var nanos = decimal.ToInt32((value - units) * NanoFactor);
return new CustomTypes.DecimalValue(units, nanos);
}
}
[!IMPORTANT] Whenever you use custom message types like this, you must document them with comments in
.proto. Other developers can then implement conversion to and from the equivalent type in their own language or framework.
Nullable types
The Protobuf code generation for C# uses the native types, such as int for int32. So the values are always included and can’t be null.
syntax = "proto3"
import "google/protobuf/wrappers.proto"
message Person {
google.protobuf.Int32Value age = 5;
}
Protobuf will use the simple T? (for example, int?) for the generated message property. The following table shows the complete list of wrapper types with their equivalent C# type:
| C# type | Well Known Type wrapper |
|---|---|
double? | google.protobuf.DoubleValue |
float? | google.protobuf.FloatValue |
int? | google.protobuf.Int32Value |
long? | google.protobuf.Int64Value |
uint? | google.protobuf.UInt32Value |
ulong? | google.protobuf.UInt64Value |
If you need any type that is not inthe table above to be null, you need to construct your type using the oneOf consturct
syntax = "proto3";
import "google/protobuf/wrappers.proto";
import "google/protobuf/struct.proto";
package tutorial;
option csharp_namespace = "MyExample.Cat";
message Cat {
google.protobuf.Int64Value id = 1;
google.protobuf.StringValue name = 2;
NullableField profile_picture = 3;
}
message NullableField {
oneof kind {
google.protobuf.NullValue null = 1;
google.protobuf.StringValue value = 2;
}
}
Date & times
syntax = "proto3"
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
message Meeting {
string subject = 1;
google.protobuf.Timestamp time = 2;
google.protobuf.Duration duration = 3;
}
// Create Timestamp and Duration from .NET DateTimeOffset and TimeSpan
var meeting = new Meeting
{
Time = Timestamp.FromDateTimeOffset(meetingTime),
Duration = Duration.FromTimeSpan(meetingLength)
};
// Convert Timestamp and Duration to .NET DateTimeOffset and TimeSpan
DateTimeOffset time = meeting.Time.ToDateTimeOffset();
TimeSpan? duration = meeting.Duration?.ToTimeSpan()
The Timestamp type works with UTC times. DateTimeOffset values always have an offset of zero, and the DateTime.Kind property is always DateTimeKind.Utc.
The well-known types Timestamp and Duration are represented in .NET as classes.
| C# type | Protobuf well-known type |
|---|---|
DateTimeOffset | google.protobuf.Timestamp |
DateTime | google.protobuf.Timestamp |
TimeSpan | google.protobuf.Duration |
Guids
The Guid type, also known as UUID on other platforms, is not directly supported by Protobuf. As a result, there is no recognized type for it. The recommended method is to treat Guid values as a string field and use the standard 8-4-4-4-12 hexadecimal format, such as 45a9fda3-bd01-47a9-8460-c1cd7484b0b3, which can be parsed by all languages and platforms. It is not advisable to use a bytes field for Guid values as it can cause unexpected behavior when Protobuf interacts with other platforms like Java due to endianness issues.
Any
Any message type lets you use messages as embedded types without having their .proto definition. An Any contains an arbitrary serialized message as bytes, along with a URL that acts as a globally unique identifier for and resolves to that message’s type. To use the Any type, you need to import google/protobuf/any.proto.
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}